Skip to content

fix: stop push notifications after session expires without explicit logout - #312

Merged
deaflynx merged 15 commits into
thingsboard:develop/1.9.0from
deaflynx:fix/push-notifications-after-logout-304
Sep 10, 2026
Merged

fix: stop push notifications after session expires without explicit logout#312
deaflynx merged 15 commits into
thingsboard:develop/1.9.0from
deaflynx:fix/push-notifications-after-logout-304

Conversation

@deaflynx

Copy link
Copy Markdown
Contributor

Fixes #304

Merge of the CE fix (thingsboard/flutter_thingsboard_app#248) into PE.

Problem

When the session ends without an explicit logout (refresh token expires while the app is unused), the FCM token stays registered on the platform and the device keeps receiving alarm push notifications after the automatic logout.

Solution

  • Persist a push_notifications_registered flag (TbStorage) whenever the FCM token is registered with the platform — on a fresh saveMobileSession and when init() finds an existing valid session (covers installs upgrading to this version).
  • New NotificationService.handleSessionExpired(): if the flag is set, runs the full push cleanup and clears the flag. Deleting the local FCM token is what actually stops delivery — the JWT is already invalid at this point, so the server-side removeMobileSession call usually cannot succeed; subsequent pushes to the deleted token bounce with UNREGISTERED and the platform purges the mobile session on the next delivery attempt.
  • Login.handleUserLoaded() triggers the cleanup whenever it detects an unauthenticated client with Firebase configured. Because the trigger is the persisted flag (not the in-memory login state), it works on cold start — the main reported scenario (app killed, token expired days ago) — as well as when the expiry happens while the app is running. It is a no-op on fresh installs, after manual logout, and on subsequent logged-out launches.
  • NotificationService.logout() now awaits removeMobileSession inside a try/catch: previously the call was fire-and-forget and produced an unhandled async error exactly in the expired-token case.

Limitations

Pushes delivered between the token expiry and the next app launch cannot be stopped from the client — closing that gap requires a platform-side change (tying mobile session lifetime to the auth session), which is being discussed separately.

Testing

  • Unit tests for the handleSessionExpired flag logic (test/utils/services/notification_service_test.dart), passing on the PE tree.
  • flutter analyze — no new findings on touched files.

@deaflynx deaflynx added the bug Something isn't working label Aug 20, 2026
@deaflynx deaflynx added this to the 1.9.0 milestone Aug 20, 2026
deaflynx and others added 13 commits August 25, 2026 11:57
…board#304)

- await Firebase init in main() so the apps.isNotEmpty guards are
  deterministic, and move the Firebase-configured guard into
  NotificationService instead of copying it at every call site
- persist the push-registration flag via DatabaseKeys and
  ILocalDatabaseService, checked with containsKey instead of a string
  sentinel
- rename handleSessionExpired to cleanUpStalePushRegistration and split
  the local teardown out of logout() into _cleanupPushRegistration()
- handle failures of the remaining fire-and-forget removeMobileSession
  calls (token refresh listener, _resetToken) and clear the cached FCM
  token after deletion
- fix TbContext.logout() calling NotificationService.init() instead of
  logout()
- drive the real cleanup path in tests via injectable Firebase
  collaborators and pin the persisted key contract
…logout-304' into fix/push-notifications-after-logout-304

# Conflicts:
#	lib/main.dart
…d#304)

- move the teardown's try/catch into _tearDownLocalPushState() so an
  offline logout still clears the client session; the flag stays set and
  the next launch retries
- serialize the stale cleanup: concurrent calls share one run and init()
  awaits it, so a fast auto-login cannot register a token that is being
  deleted
- save a refreshed FCM token even when no previous token was resolved
- give each _getAndSaveToken() path a single owner of the flag write
- read the registration flag by value, camelCase its persisted key
- declare dio under dependencies (imported by production code)
- tests: cover init() flag writes, token rotation, token refresh, cleanup
  ordering/deduplication and offline logout; helper registers its own
  getIt teardown
- catch a failing registration-flag read inside the stale cleanup so the
  shared future never fails and init() still registers afterwards
- document that init() only waits for a cleanup already in flight, and
  that the ordering relies on the service being a locator singleton
- tests: rotation without a fresh token, provisional permission, failed
  save in the refresh listener, retry after an interrupted teardown,
  unreadable flag; extract the repeated stub/verify helpers
…logout-304' into fix/push-notifications-after-logout-304
…t-switch

fix: make QR-code login and endpoint switch reliable
…external-url-link

fix(dashboard): display dashboard images configured via external URL link
…ifications-after-logout-304

# Conflicts:
#	lib/utils/services/notification_service.dart
…ifications-after-logout-304

# Conflicts:
#	lib/core/auth/login/provider/login_provider.g.dart
#	lib/utils/services/notification_service.dart
…logout-304' into fix/push-notifications-after-logout-304

# Conflicts:
#	lib/core/auth/login/provider/login_provider.g.dart
@deaflynx
deaflynx merged commit 9a75b71 into thingsboard:develop/1.9.0 Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant